/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package multiversiontechnicaldocumentation;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Stack;

/**
 * Multiversion technical document reader.
 *
 * The tokens of a multiversion technical document are: COMMAND, DATA. A token
 * begins always on a new line. The document source must resemble very much the
 * source code of an imperative programming language, though the main part of
 * the content is data. Though this format is very restricting in comparison
 * with XML for example, it is enough for the most of technical documentation.
 *
 * See RFC documents in txt format on http://www.ietf.org/ for comparison.
 *
 * The COMMAND token must have the following format: ID, ARG-LIST. A COMMAND ID
 * is written in upper case. Example COMMANDs:
 * <ul>
 * <li>VERSION</li>
 * <li>LOGICALSECTION</li>
 * <li>LINEBREAK</li>
 * <li>LIST</li>
 * <li>TABLE</li>
 * <li>IMAGE</li>
 * </ul>
 *
 * The ID of a logical section is used to link logical sections between
 * different document versions. Thus the ID of a logical section must be unique
 * in a document version.
 *
 * The content of a logical section can be defined in two ways:
 * <ol>
 * <li><b>replace previous version</b></li>
 * <li><b>copy previous version</b></li>
 * </ol>
 * A logical section is marked with REPLACE if its content has changed. This
 * means:
 * <blockquote>
 * if a logical section contains a list of child logical sections then it should
 * be marked with REPLACE only if the list is changed (the content of child
 * logical sections does not matter)
 * </blockquote>
 *
 * Outside logical sections is not allowed to put documentation.
 *
 * A DATA token must not have newline characters (only LINEBREAK commands to
 * create newline). The reader eliminates newline character encountered in 
 * DATA tokens. (The writer inserts newline characters to break long lines 
 * but only for pretty printing.)
 *
 * @author VadimBaitoiu
 */
public class Reader {

    private int parseBeginIndex;

    private static class Token {

        public static enum Type {

            COMMAND__VERSION,
            COMMAND__LOGICALSECTION,
            DATA
        }

        private Type type;
        private String text;

        public Type getType() {
            return type;
        }

        public void setType(Type type) {
            this.type = type;
        }

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public Token(Type type, String text) {
            this.type = type;
            this.text = text;
        }

    }

    /**
     *
     * @param file
     * @return
     * @throws java.io.FileNotFoundException
     * @throws multiversiontechnicaldocumentation.DocumentStructureException
     * @throws multiversiontechnicaldocumentation.DocumentCommandException
     */
    public Document read(File file) throws FileNotFoundException, IOException, DocumentStructureException, DocumentCommandException {
        FileInputStream fis = new FileInputStream(file);
        InputStreamReader isr = new InputStreamReader(fis);
        return this.read(isr);
    }

    /**
     *
     *
     * NOTE You can "connect" a InputStreamReader instance to a string:
     * InputStreamReader isr = new
     * InputStreamReader(IOUtils.toInputStream(myString));
     *
     * @param isr input stream reader
     * @return
     * @throws java.io.IOException
     * @throws multiversiontechnicaldocumentation.DocumentStructureException
     * @throws multiversiontechnicaldocumentation.DocumentCommandException
     */
    public Document read(InputStreamReader isr) throws IOException, DocumentStructureException, DocumentCommandException {
        ArrayList<Token> tokenList = tokenize(isr);
        Document document = parse(tokenList);
        return document;
    }

    /**
     * Tokenize input stream.
     *
     * Tokens are separated by newline characters. (But not always a newline
     * generates a new token -- see token DATA.)
     *
     * @param isr
     * @return
     * @throws IOException
     */
    private static ArrayList<Token> tokenize(InputStreamReader isr) throws IOException {
        ArrayList<Token> tokenList = new ArrayList<>();
        java.io.Reader in = new BufferedReader(isr);
        StringBuilder line = null;
        StringBuilder dataTokenText = null;

        boolean endOfStream = false;
        while (!endOfStream) {
            int ch;
            if ((ch = in.read()) <= -1) {
                endOfStream = true;
            }
            boolean endOfLine = false;
            if (endOfStream) {
                endOfLine = true;
            } else {
                if (ch == '\r' || ch == '\n') {
                    endOfLine = true;
                }
            }
            if (endOfLine) {
                if (line != null) {
                    String s = line.toString();
                    boolean isCommand = false;
                    Token.Type commandType = null;
                    if (s.startsWith("LOGICALSECTION")) {
                        isCommand = true;
                        commandType = Token.Type.COMMAND__LOGICALSECTION;
                    } else if (s.startsWith("VERSION")) {
                        isCommand = true;
                        commandType = Token.Type.COMMAND__VERSION;
                    }
                    if (isCommand) {
                        if (dataTokenText != null) {
                            Token token = new Token(Token.Type.DATA, dataTokenText.toString());
                            tokenList.add(token);
                            dataTokenText = null;
                        }

                        Token token = new Token(commandType, s);
                        tokenList.add(token);
                    } else {
                        // data
                        if (dataTokenText == null) {
                            dataTokenText = new StringBuilder();
                        }
                        dataTokenText.append(line);
                        dataTokenText.append('\n');
                    }
                    line = null;
                }
                if (endOfStream) {
                    if (dataTokenText != null) {
                        Token token = new Token(Token.Type.DATA, dataTokenText.toString());
                        tokenList.add(token);
                        dataTokenText = null;
                    }
                }
            } else {
                if (line == null) {
                    line = new StringBuilder();
                }
                line.append((char) ch);
            }
        }

        return tokenList;
    }

    /**
     * "Macro"-read. Convert token list to Document.
     *
     * @param tokenList
     * @return
     * @throws DocumentStructureException
     */
    private Document parse(ArrayList<Token> tokenList) throws DocumentStructureException, DocumentCommandException {
        Document document = new Document();
        Stack<Document.LogicalSection> stack = new Stack<>();
        SimpleDateFormat sdf = new SimpleDateFormat(Writer.DOCUMENT_VERSION_TIME_FORMAT);

        for (Token token : tokenList) {
            String tokenText = token.getText();
            switch (token.getType()) {
                case COMMAND__VERSION:
                    // create version instance
                    Document.Version documentVersion = new Document.Version();
                    document.getVersionList().add(documentVersion);

                    // read time
                    ParsePosition parsePosition = new ParsePosition("VERSION".length());
                    Date date = sdf.parse(tokenText, parsePosition);
                    if (date != null) {
                        documentVersion.setTime(date.getTime());

                        // read title
                        this.parseBeginIndex = parsePosition.getIndex();
                        String title = this.parseTitle(tokenText);
                        documentVersion.setTitle(title);

                        // create root logical section
                        Document.LogicalSection rootLogicalSection = new Document.LogicalSection();
                        rootLogicalSection.setContentMode(true);
                        documentVersion.setRoot(rootLogicalSection);
                        stack.push(rootLogicalSection);
                    } else {
                        throw new DocumentStructureException("Invalid version time");
                    }
                    break;
                case COMMAND__LOGICALSECTION:
                    Document.Version mostRecentDocumentVersion = document.getMostRecentVersion();
                    if (mostRecentDocumentVersion != null) {
                        this.parseBeginIndex = "LOGICALSECTION".length();
                        long ID = this.parseID(tokenText);
                        if (ID != -1L) {
                            // logical section begin
                            if (!stack.isEmpty()) {
                                Document.LogicalSection logicalSection = new Document.LogicalSection();
                                logicalSection.setID(ID);
                                Document.LogicalSection parentLogicalSection = stack.peek();
                                parentLogicalSection.appendChild(logicalSection);
                                stack.push(logicalSection);

                                // read title
                                String title = this.parseTitle(tokenText);
                                logicalSection.setTitle(title);

                                // read content mode
                                boolean contentMode = this.parseLogicalSectionContentMode(tokenText);
                                logicalSection.setContentMode(contentMode);
                            } else {
                                throw new DocumentStructureException(
                                        "Invalid logical section hierarchy: logical section without parent logical section");
                            }
                        } else {
                            // logical section end
                            if (!stack.isEmpty()) {
                                stack.pop();
                            } else {
                                throw new DocumentStructureException(
                                        "Invalid logical section hierarchy: logical section end without logical section begin");
                            }
                        }
                    } else {
                        throw new DocumentStructureException(
                                "Invalid logical section hierarchy: undefined version");
                    }
                    break;
                case DATA:
                    if (!stack.isEmpty()) {
                        Document.LogicalSection logicalSection = stack.peek();
                        logicalSection.setContent(token.getText());
                    } else {
                        throw new DocumentStructureException(
                                "Invalid logical section hierarchy: data without logical section");
                    }
                    break;
                default:
                    break;
            }
        }
        return document;
    }

    /**
     * Parse version title or logical section title.
     *
     * A title is enclosed in "".
     *
     * @param s
     * @return
     */
    private String parseTitle(String s) {
        String title = null;
        int titleBeginIndex = -1, titleEndIndex = -1;
        int i;

        for (i = this.parseBeginIndex; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (ch == '"') {
                if (titleBeginIndex < 0) {
                    titleBeginIndex = i;
                } else {
                    if (titleEndIndex < 0) {
                        titleEndIndex = i;
                        break;
                    }
                }
            }
        }

        if (titleEndIndex >= 0) {
            title = s.substring(titleBeginIndex + 1, titleEndIndex);
            this.parseBeginIndex = i + 1;
        }

        return title;
    }

    /**
     * Parse ID.
     *
     * NOTE The ID must be representable on int32.
     *
     * @param s -1 -> invalid
     * @return
     */
    private long parseID(String s) {
        long ID = -1;
        boolean flag = false;
        int i;

        for (i = this.parseBeginIndex; i < s.length(); i++) {
            int ch = (int) s.charAt(i);
            if ((ch >= '0') && (ch <= '9')) {
                if (!flag) {
                    ID = 0;
                    flag = true;
                }
                ID = 10L * ID + (ch - '0');
            } else {
                if (flag) {
                    break;
                }
            }
        }

        if (flag) {
            this.parseBeginIndex = i;
        }

        return ID;
    }

    private boolean parseLogicalSectionContentMode(String s) throws DocumentCommandException {
        boolean contentMode;

        // skip whitespace
        while (this.parseBeginIndex < s.length()) {
            int ch = (int) s.charAt(this.parseBeginIndex);
            if (ch == ' ') {
                this.parseBeginIndex++;
            } else {
                break;
            }
        }

        if (s.startsWith("REPLACE", this.parseBeginIndex)) {
            contentMode = true;
        } else {
            if (s.startsWith("COPY", this.parseBeginIndex)) {
                contentMode = false;
            } else {
                throw new DocumentCommandException("Invalid logical section content mode");
            }
        }

        return contentMode;
    }
}
